﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;

namespace fastJSON
{
    /// <summary>
    /// This class encodes and decodes JSON strings.
    /// Spec. details, see http://www.json.org/
    /// 
    /// JSON uses Arrays and Objects. These correspond here to the datatypes ArrayList and Hashtable.
    /// All numbers are parsed to doubles.
    /// </summary>
    internal class JsonParser
    {
        private enum Token
        {
            None = -1, // Used to denote no Lookahead available
            Curly_Open,
            Curly_Close,
            Squared_Open,
            Squared_Close,
            Colon,
            Comma,
            String,
            Number,
            True,
            False,
            Null
        }

        private readonly char[] json;
        private readonly StringBuilder s = new StringBuilder();
        private Token lookAheadToken = Token.None;
        private int index;

        internal JsonParser(string json)
        {
            this.json = json.ToCharArray();
        }

        public object Decode()
        {
            return ParseValue();
        }

        private Dictionary<string, object> ParseObject()
        {
            var table = new Dictionary<string, object>();
            ConsumeToken(); // {
            while (true)
            {
                switch (LookAhead())
                {
                    case Token.Comma:
                        ConsumeToken();
                        break;
                    case Token.Curly_Close:
                        ConsumeToken();
                        return table;
                    default:
                        {
                            // name
                            string name = ParseString();
                            // :
                            if (NextToken() != Token.Colon)
                            {
                                throw new Exception("Expected colon at index " + this.index);
                            }
                            // value
                            object value = ParseValue();
                            table[name] = value;
                        }
                        break;
                }
            }
        }

#if SILVERLIGHT
        private List<object> ParseArray()
        {
            List<object> array = new List<object>();
#else
        private ArrayList ParseArray()
        {
            var array = new ArrayList();
#endif
            ConsumeToken(); // [
            while (true)
            {
                switch (LookAhead())
                {
                    case Token.Comma:
                        ConsumeToken();
                        break;
                    case Token.Squared_Close:
                        ConsumeToken();
                        return array;
                    default:
                        {
                            array.Add(ParseValue());
                        }
                        break;
                }
            }
        }

        private object ParseValue()
        {
            switch (LookAhead())
            {
                case Token.Number:
                    return ParseNumber();
                case Token.String:
                    return ParseString();
                case Token.Curly_Open:
                    return ParseObject();
                case Token.Squared_Open:
                    return ParseArray();
                case Token.True:
                    ConsumeToken();
                    return true;
                case Token.False:
                    ConsumeToken();
                    return false;
                case Token.Null:
                    ConsumeToken();
                    return null;
            }
            throw new Exception("Unrecognized token at index" + this.index);
        }

        private string ParseString()
        {
            ConsumeToken(); // "
            this.s.Length = 0;
            int runIndex = -1;
            while (this.index < this.json.Length)
            {
                char c = this.json[this.index++];
                if (c == '"')
                {
                    if (runIndex != -1)
                    {
                        if (this.s.Length == 0)
                        {
                            return new string(this.json, runIndex, this.index - runIndex - 1);
                        }
                        this.s.Append(this.json, runIndex, this.index - runIndex - 1);
                    }
                    return this.s.ToString();
                }
                if (c != '\\')
                {
                    if (runIndex == -1)
                    {
                        runIndex = this.index - 1;
                    }
                    continue;
                }
                if (this.index == this.json.Length)
                {
                    break;
                }
                if (runIndex != -1)
                {
                    this.s.Append(this.json, runIndex, this.index - runIndex - 1);
                    runIndex = -1;
                }
                switch (this.json[this.index++])
                {
                    case '"':
                        this.s.Append('"');
                        break;
                    case '\\':
                        this.s.Append('\\');
                        break;
                    case '/':
                        this.s.Append('/');
                        break;
                    case 'b':
                        this.s.Append('\b');
                        break;
                    case 'f':
                        this.s.Append('\f');
                        break;
                    case 'n':
                        this.s.Append('\n');
                        break;
                    case 'r':
                        this.s.Append('\r');
                        break;
                    case 't':
                        this.s.Append('\t');
                        break;
                    case 'u':
                        {
                            int remainingLength = this.json.Length - this.index;
                            if (remainingLength < 4)
                            {
                                break;
                            }
                            // parse the 32 bit hex into an integer codepoint
                            uint codePoint = ParseUnicode(this.json[this.index], this.json[this.index + 1], this.json[this.index + 2], this.json[this.index + 3]);
                            this.s.Append((char) codePoint);
                            // skip 4 chars
                            this.index += 4;
                        }
                        break;
                }
            }
            throw new Exception("Unexpectedly reached end of string");
        }

        private uint ParseSingleChar(char c1, uint multipliyer)
        {
            uint p1 = 0;
            if (c1 >= '0' && c1 <= '9')
            {
                p1 = (uint) (c1 - '0') * multipliyer;
            }
            else if (c1 >= 'A' && c1 <= 'F')
            {
                p1 = (uint) ((c1 - 'A') + 10) * multipliyer;
            }
            else if (c1 >= 'a' && c1 <= 'f')
            {
                p1 = (uint) ((c1 - 'a') + 10) * multipliyer;
            }
            return p1;
        }

        private uint ParseUnicode(char c1, char c2, char c3, char c4)
        {
            uint p1 = ParseSingleChar(c1, 0x1000);
            uint p2 = ParseSingleChar(c2, 0x100);
            uint p3 = ParseSingleChar(c3, 0x10);
            uint p4 = ParseSingleChar(c4, 1);
            return p1 + p2 + p3 + p4;
        }

        private string ParseNumber()
        {
            ConsumeToken();
            // Need to start back one place because the first digit is also a token and would have been consumed
            int startIndex = this.index - 1;
            do
            {
                char c = this.json[this.index];
                if ((c >= '0' && c <= '9') || c == '.' || c == '-' || c == '+' || c == 'e' || c == 'E')
                {
                    if (++this.index == this.json.Length)
                    {
                        throw new Exception("Unexpected end of string whilst parsing number");
                    }
                    continue;
                }
                break;
            } while (true);
            return new string(this.json, startIndex, this.index - startIndex);
        }

        private Token LookAhead()
        {
            if (this.lookAheadToken != Token.None)
            {
                return this.lookAheadToken;
            }
            return this.lookAheadToken = NextTokenCore();
        }

        private void ConsumeToken()
        {
            this.lookAheadToken = Token.None;
        }

        private Token NextToken()
        {
            Token result = this.lookAheadToken != Token.None ? this.lookAheadToken : NextTokenCore();
            this.lookAheadToken = Token.None;
            return result;
        }

        private Token NextTokenCore()
        {
            char c;
            // Skip past whitespace
            do
            {
                c = this.json[this.index];
                if (c > ' ')
                {
                    break;
                }
                if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
                {
                    break;
                }
            } while (++this.index < this.json.Length);
            if (this.index == this.json.Length)
            {
                throw new Exception("Reached end of string unexpectedly");
            }
            c = this.json[this.index];
            this.index++;
            //if (c >= '0' && c <= '9')
            //    return Token.Number;
            switch (c)
            {
                case '{':
                    return Token.Curly_Open;
                case '}':
                    return Token.Curly_Close;
                case '[':
                    return Token.Squared_Open;
                case ']':
                    return Token.Squared_Close;
                case ',':
                    return Token.Comma;
                case '"':
                    return Token.String;
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                case '-':
                case '+':
                case '.':
                    return Token.Number;
                case ':':
                    return Token.Colon;
                case 'f':
                    if (this.json.Length - this.index >= 4 &&
                        this.json[this.index + 0] == 'a' &&
                        this.json[this.index + 1] == 'l' &&
                        this.json[this.index + 2] == 's' &&
                        this.json[this.index + 3] == 'e')
                    {
                        this.index += 4;
                        return Token.False;
                    }
                    break;
                case 't':
                    if (this.json.Length - this.index >= 3 &&
                        this.json[this.index + 0] == 'r' &&
                        this.json[this.index + 1] == 'u' &&
                        this.json[this.index + 2] == 'e')
                    {
                        this.index += 3;
                        return Token.True;
                    }
                    break;
                case 'n':
                    if (this.json.Length - this.index >= 3 &&
                        this.json[this.index + 0] == 'u' &&
                        this.json[this.index + 1] == 'l' &&
                        this.json[this.index + 2] == 'l')
                    {
                        this.index += 3;
                        return Token.Null;
                    }
                    break;
            }
            throw new Exception("Could not find token at index " + --this.index);
        }
    }
}